﻿using System;
using System.IO;
using System.Reflection;
using System.Collections.Generic;

namespace Narwhunderful.Aphelia.Framework.IO.Serialization.Xml
{

    public sealed class XmlPropertyDeserializer : IPropertyDeserializer
    {
        private readonly IXmlReader _reader;
        private IDictionary<int, ComplexProperty> complexItems = new Dictionary<int, ComplexProperty>();

        public XmlPropertyDeserializer(IXmlReader reader)
        {
            _reader = reader;
        }

        public void Open(Stream stream)
        {
            _reader.Open(stream);
        }

        public Property Deserialize()
        {
            // give the first valid tag back
            string elementName = _reader.ReadElement();

            // In what xml tag is the property saved
            PropertyTag propertyTag = GetPropertyTag(elementName);

            // check if the property was found
            if (propertyTag == PropertyTag.Unknown) return null;

            Property result = Deserialize(propertyTag, null);
            return result;
        }

        public void Close()
        {
            _reader.Close();
        }

        private Property Deserialize(PropertyTag propertyTag, Type expectedType)
        {
            // Establish the property name
            string propertyName = _reader.GetAttributeAsString(Attributes.Name);

            // Establish the property type
            Type propertyType = _reader.GetAttributeAsType(Attributes.Type);

            // id propertyType is not defined, we'll take the expectedType)
            if (propertyType == null)
            {
                propertyType = expectedType;
            }

            // create the property
            Property property = CreateProperty(propertyTag, propertyName, propertyType);
            if (property == null)
                return null;

            // Null property?
            var nullProperty = property as NullProperty;
            if (nullProperty != null)
            {
                return nullProperty;
            }

            // is it simple property?
            var simpleProperty = property as SimpleProperty;
            if (simpleProperty != null)
            {
                ParseSimpleProperty(_reader, simpleProperty);
                return simpleProperty;
            }

            var multiDimensionalArrayProperty = property as MultiDimensionalArrayProperty;
            if (multiDimensionalArrayProperty != null)
            {
                ParseMultiDimensionalArrayProperty(multiDimensionalArrayProperty);
                return multiDimensionalArrayProperty;
            }

            var singleDimensionalArrayProperty = property as SingleDimensionalArrayProperty;
            if (singleDimensionalArrayProperty != null)
            {
                ParseSingleDimensionalArrayProperty(singleDimensionalArrayProperty);
                return singleDimensionalArrayProperty;
            }

            var dictionaryProperty = property as DictionaryProperty;
            if (dictionaryProperty != null)
            {
                ParseDictionaryProperty(dictionaryProperty);
                return dictionaryProperty;
            }

            var collectionProperty = property as CollectionProperty;
            if (collectionProperty != null)
            {
                ParseCollectionProperty(collectionProperty);
                return collectionProperty;
            }

            var complexReferenceProperty = property as ComplexReferenceProperty;
            if (complexReferenceProperty != null)
            {
                ParseComplexReferenceProperty(complexReferenceProperty);
                return complexReferenceProperty;
            }

            var complexProperty = property as ComplexProperty;
            if (complexProperty != null)
            {
                ParseComplexProperty(complexProperty);
                return complexProperty;
            }

            return property;
        }

        private void ParseCollectionProperty(CollectionProperty property)
        {
            // ElementType
            property.ElementType = property.Type != null ? TypeInfo.GetTypeInfo(property.Type).ElementType : null;

            foreach (string subElement in _reader.ReadSubElements())
            {
                if (subElement == SubElements.Properties)
                {
                    // Properties
                    ReadProperties(property.Properties, property.Type);
                    continue;
                }

                if (subElement == SubElements.Items)
                {
                    // Items
                    ReadItems(property.Items, property.ElementType);
                }
            }
        }

        private void ParseDictionaryProperty(DictionaryProperty property)
        {
            if (property.Type != null)
            {
                var typeInfo = TypeInfo.GetTypeInfo(property.Type);
                property.KeyType = typeInfo.KeyType;
                property.ValueType = typeInfo.ElementType;
            }

            foreach (string subElement in _reader.ReadSubElements())
            {
                if (subElement == SubElements.Properties)
                {
                    // Properties
                    ReadProperties(property.Properties, property.Type);
                    continue;
                }
                if (subElement == SubElements.Items)
                {
                    // Items
                    ReadDictionaryItems(property.Items, property.KeyType, property.ValueType);
                }
            }
        }

        private void ReadDictionaryItems(IList<KeyValueItem> items, Type expectedKeyType, Type expectedValueType)
        {
            foreach (string subElement in _reader.ReadSubElements())
            {
                if (subElement == SubElements.Item)
                {
                    ReadDictionaryItem(items, expectedKeyType, expectedValueType);
                }
            }
        }

        private void ReadDictionaryItem(IList<KeyValueItem> items, Type expectedKeyType, Type expectedValueType)
        {
            Property keyProperty = null;
            Property valueProperty = null;
            foreach (string subElement in _reader.ReadSubElements())
            {
                // check if key and value was found
                if (keyProperty != null && valueProperty != null) break;

                // check if valid tag was found
                PropertyTag propertyTag = GetPropertyTag(subElement);
                if (propertyTag == PropertyTag.Unknown) continue;

                // items are as pair key-value defined

                // first is always the key
                if (keyProperty == null)
                {
                    // Key was not defined yet (the first item was found)
                    keyProperty = Deserialize(propertyTag, expectedKeyType);
                    continue;
                }

                // key was defined (the second item was found)
                valueProperty = Deserialize(propertyTag, expectedValueType);
            }

            // create the item
            var item = new KeyValueItem(keyProperty, valueProperty);
            items.Add(item);
        }

        private void ParseMultiDimensionalArrayProperty(MultiDimensionalArrayProperty property)
        {
            property.ElementType = property.Type != null ? TypeInfo.GetTypeInfo(property.Type).ElementType : null;

            foreach (string subElement in _reader.ReadSubElements())
            {
                if (subElement == SubElements.Dimensions)
                {
                    // Read dimensions
                    ReadDimensionInfos(property.DimensionInfos);
                }

                if (subElement == SubElements.Items)
                {
                    // Read items
                    ReadMultiDimensionalArrayItems(property.Items, property.ElementType);
                }
            }
        }

        private void ReadMultiDimensionalArrayItems(IList<MultiDimensionalArrayItem> items, Type expectedElementType)
        {
            foreach (string subElement in _reader.ReadSubElements())
            {
                if (subElement == SubElements.Item)
                {
                    ReadMultiDimensionalArrayItem(items, expectedElementType);
                }
            }
        }

        private void ReadMultiDimensionalArrayItem(IList<MultiDimensionalArrayItem> items, Type expectedElementType)
        {
            int[] indexes = _reader.GetAttributeAsArrayOfInt(Attributes.Indexes);
            foreach (string subElement in _reader.ReadSubElements())
            {
                PropertyTag propertyTag = GetPropertyTag(subElement);
                if (propertyTag == PropertyTag.Unknown) continue;

                Property value = Deserialize(propertyTag, expectedElementType);
                var item = new MultiDimensionalArrayItem(indexes, value);
                items.Add(item);
            }
        }

        private void ReadDimensionInfos(IList<DimensionInfo> dimensionInfos)
        {
            foreach (string subElement in _reader.ReadSubElements())
            {
                if (subElement == SubElements.Dimension)
                {
                    ReadDimensionInfo(dimensionInfos);
                }
            }
        }

        private void ReadDimensionInfo(IList<DimensionInfo> dimensionInfos)
        {
            var info = new DimensionInfo();
            info.Length = _reader.GetAttributeAsInt(Attributes.Length);
            info.LowerBound = _reader.GetAttributeAsInt(Attributes.LowerBound);
            dimensionInfos.Add(info);
        }

        private void ParseSingleDimensionalArrayProperty(SingleDimensionalArrayProperty property)
        {
            // ElementType
            property.ElementType = property.Type != null ? TypeInfo.GetTypeInfo(property.Type).ElementType : null;

            // LowerBound
            property.LowerBound = _reader.GetAttributeAsInt(Attributes.LowerBound);

            // Items
            foreach (string subElement in _reader.ReadSubElements())
            {
                if (subElement == SubElements.Items)
                {
                    ReadItems(property.Items, property.ElementType);
                }
            }
        }

        private void ReadItems(ICollection<Property> items, Type expectedElementType)
        {
            foreach (string subElement in _reader.ReadSubElements())
            {
                PropertyTag propertyTag = GetPropertyTag(subElement);
                if (propertyTag != PropertyTag.Unknown)
                {
                    // Property is found
                    Property subProperty = Deserialize(propertyTag, expectedElementType);
                    items.Add(subProperty);
                }
            }
        }

        private void ParseComplexReferenceProperty(ComplexReferenceProperty property)
        {
            int complexReferenceId = _reader.GetAttributeAsInt(Attributes.ComplexReferenceId);
            ComplexProperty target;
            if (complexItems.TryGetValue(complexReferenceId, out target))
                property.ReferenceTarget = target;
            else
            {
                string message = string.Format("{0}-parser : Cannot find <{6} {4}='{5}'/>  when resolving <{1} {2} ='{3}' {4}='{5}'/>",
                    GetType().Name,
                    Elements.ComplexObjectReference,
                    Attributes.Name, property.Name,
                    Attributes.ComplexReferenceId, complexReferenceId,
                    Elements.ComplexObject);
                throw new FormatException(message);
            }
        }

        private void ParseComplexProperty(ComplexProperty property)
        {
            property.ComplexReferenceId = _reader.GetAttributeAsInt(Attributes.ComplexReferenceId);

            // remember for later resolution
            if (property.IsReferencedMoreThanOnce)
                complexItems.Add(property.ComplexReferenceId, property);

            foreach (string subElement in _reader.ReadSubElements())
            {
                if (subElement == SubElements.Properties)
                {
                    ReadProperties(property.Properties, property.Type);
                }
            }
        }

        private void ReadProperties(PropertyCollection properties, Type ownerType)
        {
            foreach (string subElement in _reader.ReadSubElements())
            {
                PropertyTag propertyTag = GetPropertyTag(subElement);
                if (propertyTag != PropertyTag.Unknown)
                {
                    // check if the property with the name exists
                    string subPropertyName = _reader.GetAttributeAsString(Attributes.Name);
                    if (string.IsNullOrEmpty(subPropertyName)) continue;

                    // estimating the propertyInfo
                    PropertyInfo subPropertyInfo = ownerType.GetProperty(subPropertyName);

                    Property subProperty = Deserialize(propertyTag, subPropertyInfo.PropertyType);
                    properties.Add(subProperty);
                }
            }
        }

        private void ParseSimpleProperty(IXmlReader reader, SimpleProperty property)
        {
            property.Value = _reader.GetAttributeAsObject(Attributes.Value, property.Type);
        }

        private static Property CreateProperty(PropertyTag tag, string propertyName, Type propertyType)
        {
            switch (tag)
            {
                case PropertyTag.Simple:
                    return new SimpleProperty(propertyName, propertyType);
                case PropertyTag.Complex:
                    return new ComplexProperty(propertyName, propertyType);
                case PropertyTag.ComplexReference:
                    return new ComplexReferenceProperty(propertyName);
                case PropertyTag.Collection:
                    return new CollectionProperty(propertyName, propertyType);
                case PropertyTag.Dictionary:
                    return new DictionaryProperty(propertyName, propertyType);
                case PropertyTag.SingleArray:
                    return new SingleDimensionalArrayProperty(propertyName, propertyType);
                case PropertyTag.MultiArray:
                    return new MultiDimensionalArrayProperty(propertyName, propertyType);
                case PropertyTag.Null:
                    return new NullProperty(propertyName);
                default:
                    return null;
            }
        }

        private static PropertyTag GetPropertyTag(string name)
        {
            if (name == Elements.SimpleObject) return PropertyTag.Simple;
            if (name == Elements.ComplexObject) return PropertyTag.Complex;
            if (name == Elements.Collection) return PropertyTag.Collection;
            if (name == Elements.SingleArray) return PropertyTag.SingleArray;
            if (name == Elements.Null) return PropertyTag.Null;
            if (name == Elements.Dictionary) return PropertyTag.Dictionary;
            if (name == Elements.MultiArray) return PropertyTag.MultiArray;
            if (name == Elements.ComplexObjectReference) return PropertyTag.ComplexReference;

            return PropertyTag.Unknown;
        }

        private enum PropertyTag
        {
            Unknown = 0,
            Simple,
            Complex,
            Collection,
            Dictionary,
            SingleArray,
            MultiArray,
            Null,
            ComplexReference
        }
    }

}
